//https://gitee.com/devcpp/cgames    anbangli@foxmail.com     GNU GPL v3
//cgame8(bricks)v2.cpp  打砖块    版本2：优化程序，加快绘制地图的速度

#include <iostream>
#include <cstdlib>
#include <ctime>
#include <Windows.h>
#include <conio.h>
using namespace std;

const int WD = 20, HT = 20;	//游戏地图的宽度(Width)和高度(Height)
char map[HT][WD];//地图二维数组  ' '-空白，'#'-围墙，'B'-砖块，'='-挡板，'O'-弹球
int X0 = 10, Y0 = 0;    //绘制地图时的左上角坐标原点
int barx, bary, barwd;	//挡板的左侧x位置、y位置和宽度
int x, y, vx, vy;   //弹球的坐标x和y，速度vx和vy

//控制台文本窗口游戏中需要移动光标到屏幕特定位置，自定义相应的函数 gotoxy。
//屏幕左上角为(0,0)，横向向右为 X 轴，竖向向下为 Y 轴。
//void gotoxy(short x, short y) {  //控制台窗口光标定位
//    static COORD crd;  //定义 COORD 类型(窗口坐标)的静态局部变量 crd
//    crd.X = x;
//    crd.Y = y;
//    SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE), crd);
//    //GetStdHandle获取控制台输出句柄并用SetConsoleCursorPosition设置光标位置
//    return;
//}

//void showcursor(bool visible) { //显示或隐藏光标
//    CONSOLE_CURSOR_INFO cursor = {20, visible};
////CONSOLE_CURSOR_INFO结构体包含控制台光标信息，两个成员分别表示光标厚度和是否可见
//    SetConsoleCursorInfo(GetStdHandle(STD_OUTPUT_HANDLE), &cursor);
//}

//void clrscr() {    //清空屏幕
//    HANDLE hdout = GetStdHandle(STD_OUTPUT_HANDLE);    //获取标准输出设备的句柄
//    CONSOLE_SCREEN_BUFFER_INFO csbi;    //定义表示屏幕缓冲区属性的变量
//    GetConsoleScreenBufferInfo(hdout, &csbi);  //获取标准输出设备的屏幕缓冲区属性
//    DWORD size = csbi.dwSize.X * csbi.dwSize.Y, num = 0; //定义双字节变量
//    COORD pos = {0, 0};    //表示坐标的变量（初始化为左上角(0, 0)点）
//    //把窗口缓冲区全部填充为空格并填充为默认颜色（清屏）
//    FillConsoleOutputCharacter(hdout, ' ', size, pos, &num);
//    FillConsoleOutputAttribute (hdout, csbi.wAttributes, size, pos, &num );
//    SetConsoleCursorPosition(hdout, pos);    //光标定位到窗口左上角
//}

void initMap(int &bricks) { //初始化地图和砖块，并对砖块计数
    int iy, ix;
    for (iy = 0; iy < HT; iy++) {	//行循环
        for (ix = 0; ix < WD; ix++) {  //列循环
            if (ix == 0 || ix == WD - 1 || iy == HT - 1 || iy == 0)
                map[iy][ix] = '#'; //围墙
            else
                map[iy][ix] = ' '; //内部空白区域
        }
    }
    //砖块随机地零散分布在地图内部空白区域上方部分
    bricks = 0;
    for (iy = 2; iy < 7; iy++)
        for (ix = 1; ix < WD - 1; ix++) {
            map[iy][ix] = (rand() % 2 ? 'B' : ' ');
            bricks += (map[iy][ix] == 'B');  //砖块计数
        }
}

void initBarBall() {    //初始化挡板和弹球
    //挡板初始时处于内部空白区域的底行
    barwd = 7;
    barx = WD / 2 - barwd / 2,
    bary = HT - 2;
    int j;
    for (j = 1; j < WD - 1; j++)
        map[bary][j] = ' '; //空格
    for (j = barx; j <= barx + barwd; j++)
        map[bary][j] = '='; //挡板
    //弹球初始时静止在挡板上面中央位置
    x = WD / 2;
    y = HT - 3;
    map[y][x] = 'O';
    vx = vy = 0;	//弹球的初始速度为0
}

void drawxy(int x, int y, char val) { //根据val的值绘制地图格点(x,y)
    gotoxy(X0 + x * 2, Y0 + y);
    if (val == ' ')  //空白
        printf("  ");
    else if (val == '#')  //围墙
        printf("█");
    else if (val == 'B')  //砖块
        printf("□");
    else if (val == '=')  //挡板
        printf("▓");
    else if (val == 'O') //弹球
        printf("●");
    else if (val == 'o') //失败时的弹球
        printf("○");
    else
        printf("×");
}

void drawMap() { //绘制整个地图
    int iy, ix;
    for (iy = 0; iy < HT; iy++) {	//行
        for (ix = 0; ix < WD; ix++) {	//列
            drawxy(ix, iy, map[iy][ix]);
        }
    }
}

void showMsg(int lives, int bricks, int score, int lines) {
    gotoxy(X0 + 0, Y0 + HT);
    cout << "lives: " << lives << "  bricks: " ;
    cout << bricks << "  scores: " << score  << "  ";
    for (int i = 0; i < lines; i++)	//清空下面几行的残余信息
        cout << "\n                                           ";
}

bool wantMore() {	//是否继续游戏
    int ch;
    gotoxy(X0 + 0, Y0 + HT + 1);
    //cout << "                            \r";    //清空本行
    cout << "再玩一局吗? (y/n): ";
    while ((ch = tolower(getchar())) != 'y' && ch != 'n')
        ;    //空循环体
    cin.sync();    //清空缓冲区
    gotoxy(X0 + 0, Y0 + HT + 1);
    cout << "                            \r";    //清空本行
    return (ch == 'y' ? true : false);
}

void showHelp() {
    clrscr();  //清屏
    showcursor(true);  //显示光标
    cout << "\n";
    cout << "[]  []  []  []  []  []  []  []  []  []  []  []  []\n";
    cout << "  []  []  []  []  []  []  []  []  []  []  []  []  \n";
    cout << "[]  []  []  []  []  []  []  ● []  []  []  []  []  \n";
    cout << "  []  []  []  []  []  []　　  []  []  []  []  []  \n";
    cout << "打砖块（anbangli@foxmail.com, 2021.1）\n\n";
    cout << "操作说明：\n  A← 左移  D→ 右移  空格 发射/暂停  H 帮助  ESC 退出\n\n";
    cout << "\n按回车键开始";
    getch();	//用户按键
    clrscr();  //清屏
    showcursor(false);  //隐藏光标
}

int main() {
    int lives = 3, score = 0;   //生命数，得分(胜利时可以带到下一局)
    int bricks = 0, hits = 0;   //本局的生命数，砖块数，已击中砖块数
    int loops;  //玩家按键时间与弹球运动时间间隔的倍数
    int key;   //用户按键

    showHelp();
    srand(time(0)); //初始化随机数种子
    initMap(bricks);    //初始化地图
    initBarBall();      //初始化挡板和弹球
    drawMap();    //主循环之前绘制整个地图（游戏过程中只绘制地图中发生变化的内容）
    showMsg(lives, bricks, score, 0);    //输出信息
    showcursor(0);
    while (true) { //游戏主循环
        //drawMap();    //绘制地图
        //showMsg(lives, bricks, score);    //输出信息
        if ((lives > 0 && bricks <= 0) || lives <= 0) { //胜利或失败
            gotoxy(X0, Y0 + HT);
            if (lives > 0)
                cout << "\n击碎所有砖块，您胜利了！";
            else { //失败
                cout << "\n本局失败，还有砖块尚未击中。 ";
                score = 0;  //本局失败，得分清0
            }

            if (wantMore()) {//是否继续
                initMap(bricks);    //初始化地图
                initBarBall();      //初始化挡板和弹球
                lives = 3;
                drawMap();    //绘制地图
                showMsg(lives, bricks, score, 2);
                continue;
            } else
                break;
        }

        //既非胜利也非失败，则游戏正在进行
        if (kbhit()) {  //有键盘输入
            key = getch();    //接收用户按键
            if (key <= 0 || key > 127)	//读取方向键时，第一次读得的不是实际键值
                key = getch();	//读取方向键时，第二次才读得实际键值
            if (key == ' ') {  //空格
                if (vx == 0 && vy == 0) { //开局发球。初速度随机斜向左右上方
                    vx = (rand() % 2 ? 1 : -1);
                    vy = -1;
                } else  //中途暂停
                    getch();  //按任意键继续
            } else if ((key == 75 || key == 'a' || key == 'A') && barx > 1) { //left
                //map[bary][barx + barwd] = ' '; //挡板右端空白
                drawxy(barx + barwd, bary, ' ');  //绘制挡板右端空白
                barx--;
                map[bary][barx] = '='; //挡板左端点
                drawxy(barx, bary, '=');  //绘制挡板左端点
                if (vx == 0 && vy == 0) { //开局时尚未发球，则球随之挡板移动
                    map[y][x] = ' ';
                    drawxy(x, y, ' ');
                    x--;
                    map[y][x] = 'O';
                    drawxy(x, y, 'O');
                }
            } else if ((key == 77 || key == 'd' || key == 'D') && (barx + barwd < WD - 2)) { //right
                //map[bary][barx] = ' '; //挡板左端空白
                drawxy(barx, bary, ' '); //绘制挡板左端空白
                barx++;
                map[bary][barx + barwd] = '='; //挡板右端点
                drawxy(barx + barwd, bary, '=');  //绘制挡板右端点
                if (vx == 0 && vy == 0) { //开局时尚未发球，则球随之挡板移动
                    map[y][x] = ' ';    //弹球原位置
                    drawxy(x, y, ' ');  //清除原位置的弹球
                    x++;
                    map[y][x] = 'O';    //弹球新位置
                    drawxy(x, y, 'O');  //在新位置绘制弹球
                }
            } else if (key == 'h' || key == 'H') {
                showHelp();
                drawMap();    //绘制地图
            } else if (key == 27) {    //是否退出游戏（ESC键的ASCII码为27）
                showcursor(1);
                gotoxy(0, HT + 1 );
                cout << "是否退出游戏？(y/n): ";
                while ((key = tolower(getch())) != 'y' && key != 'n')
                    ;    //空循环体
                cin.sync();    //清空缓冲区
                showMsg(lives, bricks, score, 1);   //输出信息并清空下方1行
                if (key == 'y')
                    break;
                else
                    showcursor(0);
            }
        }

        //处理弹球运动时间间隔是处理用户按键时间间隔的多倍
        Sleep(50);
        loops = (++loops) % 3;
        if (loops > 0)  //仅在loops等于0时处理弹球运动, 否则跳过
            continue;    //跳过下面的代码，执行下一循环

        hits = 0;	//在此循环中击中砖块数
        //先判断左右或竖直碰撞，如果发生判断则不再判断斜碰
        if (map[y][x + vx] == 'B') {  //横向运动方向为砖块
            map[y][x + vx] = ' ';     //打碎砖块变为空白
            drawxy(x + vx, y, ' ');	  //重绘原有砖块位置为空白
            hits++;  //横向碰撞记数
            vx = -vx;      //横向反弹
            cout << '\a';  //响铃
        }
        if (map[y][x + vx] == '#') { //围墙
            vx = -vx;    //横向反弹
            cout << '\a';
        }

        if (y + vy > bary) {    //如果下方是底部边界，则生命数减少
            map[y][x] = 'o';
            drawxy(x, y, 'o');
            lives--;    //生命数减少
            showMsg(lives, bricks, score, 0);
            if (lives > 0) { //生命值尚未减少到0
                showcursor(1);
                gotoxy(X0, Y0 + HT + 1);
                cout << "失败，按任意键继续";
                getch();
                map[y][x] = ' ';
                initBarBall();  //初始化挡板和弹球
                drawMap();
                showMsg(lives, bricks, score, 2);
            }
            continue;
        }
        if (map[y + vy][x] == 'B') { //如果竖直运动方向是砖块
            map[y + vy][x] = ' ';    //打碎砖块变为空白
            drawxy(x, y + vy, ' ');	 //重绘原有砖块位置为空白
            hits++; //竖直碰撞记数
            vy = -vy;	  //竖直反弹
            cout << '\a';
        }
        if (map[y + vy][x] == '#' || map[y + vy][x] == '=' ) { //围墙，挡板
            vy = -vy; //竖直反弹
            cout << '\a';
        }

        //未发生横向或竖直碰撞，则判断是否发生斜碰
        if (hits == 0 && map[y + vy][x + vx] == 'B') {
            map[y + vy][x + vx] = ' ';
            drawxy(x + vx, y + vy, ' ');	//重绘原有砖块位置为空白

            hits++;
            vy = -vy;
            vx = -vx;
            cout << '\a';
        }
        bricks -= hits;  //剩余砖块数
        score += hits;  //得分

        if (vx != 0 || vy != 0) {
            map[y][x] = ' '; //弹球原位置变为空
            drawxy(x, y, ' ');	//清除原位置的弹球
        }
        if ((vy < 0 && y > 1) || (vy > 0 && y < HT - 1))
            y = y + vy;
        if ((vx < 0 && x > 1) || (vx > 0 && x < WD - 2))
            x = x + vx;
        if (vx != 0 || vy != 0) {
            map[y][x] = 'O';
            drawxy(x, y, 'O');	//在新位置绘制弹球
        }

        showMsg(lives, bricks, score, 0);    //输出信息
    } //主循环结束

    cout << "\n游戏结束，谢谢使用！";
    showcursor(1);
    getch();
    return 0;
}
